package com.shutuo.callnummeiwei.core;

import com.shutuo.callnummeiwei.net.PacketConvert;
import com.shutuo.callnummeiwei.net.ServerSocketAccepter;
import com.shutuo.callnummeiwei.net.SocketWire;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import ashy.earl.common.closure.EarlCall;
import ashy.earl.common.closure.Method1_0;
import ashy.earl.common.closure.Params1;
import ashy.earl.common.data.JsonParser;
import ashy.earl.common.task.MessageLoop;
import ashy.earl.common.task.Task;
import ashy.earl.common.util.L;
import ashy.earl.common.util.ModifyList;
import ashy.earl.common.util.NetworkChangeHelper;
import ashy.earl.common.util.NetworkChangeHelper.NetworkListener;
import static ashy.earl.common.closure.Earl.bind;


public class ChannelServer {
    private static final String TAG = "ChannelServer";
    private static final int STATE_STOP = 1;
    private static final int STATE_STARTING = 2;
    private static final int STATE_STARTED = 3;
    private static final int STATE_WAIT_NETWORK = 4;
    private static final int CLIENT_REGISTER_TIMEOUT = 3000;// 3s
    private final ModifyList<ServerHandler> mServerLisOteners = new ModifyList<>();
    private final ServerSocketAccepter mSocketAccepter;
    private final NetworkChangeHelper mNetworkChangeHelper;
    private final MessageLoop mCreateLoop;
    private final long mServerVersion;
    private final PacketConvert mPacketConvert = new PacketConvert(C.CHANNEL);
    private List<ClientInfo> mClientInfoList = new ArrayList<>();
    private HashMap<String, ClientInfo> mClientInfoByIp = new HashMap<>();
    private HashMap<String, NewerClientInfo> mNewerClientInfoByIp = new HashMap<>();
    private int mPort;
    @State
    private int mState = STATE_STOP;
    private boolean mServerReady;

    private ServerSocketAccepter.AcceptListener mAcceptListener = new ServerSocketAccepter.AcceptListener() {
        @Override
        public void onNewClientSocket(Socket socket, String ip, int port) {
            newClientSocket(socket);
        }

        @Override
        public void onStateChanged(int state) {
            if (state == ServerSocketAccepter.STATE_STARTED) serverSocketSetuped();
        }
    };
    private NetworkListener mNetworkListener = new NetworkListener() {
        @Override
        public void onNetworkChanged(boolean hasActiveNetwork, String type, String name) {
            setupServer();
        }
    };
    private SocketWire.WireListener mWireListener = new SocketWire.WireListener() {
        @Override
        public void onConnected(SocketWire wire) {
            clientConnected(wire);
        }

        @Override
        public void onDisconnected(SocketWire wire, int reason, String detail) {
            clientDisconnected(wire, reason, detail);
        }

        @Override
        public void convertPacket(SocketWire wire, SocketWire.Packet packet) {
            mPacketConvert.convertPacket(packet);
        }

        @Override
        public void onNewPacket(SocketWire wire, SocketWire.Packet packet) {
            newClientMsg(wire, packet.arg0, packet.parsedData);
        }
    };

    @IntDef({STATE_STOP, STATE_STARTING, STATE_STARTED, STATE_WAIT_NETWORK})
    @Retention(RetentionPolicy.SOURCE)
    public @interface State {
    }

    public interface ServerHandler {
        void onServerStateChanged(boolean ready);

        boolean onNewClientMsg(ClientInfo clientInfo, int type, Object msg);

        void onClientStateChanged(ClientInfo clientInfo);
    }

    public static String stateToString(@State int state) {
        switch (state) {
            case STATE_STOP:
                return "stop";
            case STATE_STARTING:
                return "starting";
            case STATE_STARTED:
                return "started";
            case STATE_WAIT_NETWORK:
                return "wait-network";
            default:
                return "unknow-" + state;
        }
    }

    public ChannelServer(long mServerVersion) {
        this.mServerVersion = mServerVersion;
        mSocketAccepter = new ServerSocketAccepter(C.CHANNEL, TAG);
        mNetworkChangeHelper = NetworkChangeHelper.get();
        mCreateLoop = MessageLoop.current();
        mPacketConvert.registMsgParser(C.MSG_HELLO, C.PARSER_HELLO);
        mPacketConvert.registMsgParser(C.MSG_HELLO_ACK, C.PARSER_HELLO_ACK);
    }

    public void addClientHandler(ServerHandler handler) {
        mServerLisOteners.add(handler);
    }

    public void removeClientHandler(ServerHandler handler) {
        mServerLisOteners.remove(handler);
    }

    public void registMsg(int type, JsonParser parser) {
        if (mPort > 0) throw new IllegalAccessError("Must access before setup!");
        mPacketConvert.registMsgParser(type, parser);
    }

    public boolean isStoped() {
        return mState == STATE_STOP;
    }

    public void setup(int port) {
        if (mPort == port) return;
        if (port < 1024) {
            throw new IllegalArgumentException("port < 1024!");
        }
        if (port >= 65535) {
            throw new IllegalArgumentException("port >= 65535!");
        }
        teardown();
        mPort = port;
        mNetworkChangeHelper.addNetworkListener(mNetworkListener);
        setupServer();
    }

    public List<ClientInfo> getClientInfos() {
        return mClientInfoList;
    }

    public void sendMsgToClient(ClientInfo c, int type, Object msg) {
        if (mState != STATE_STARTED) {
            if (L.loggable(C.CHANNEL, L.WARN)) {
                L.w(C.CHANNEL, "%-15s~ can't send msg by state:%s", TAG, stateToString(mState));
            }
            return;
        }
        if (c.socketWire == null) {
            L.w(C.CHANNEL, "%s~ sendMsgToClient ignored by socket wire not ready!" +
                    " client:%s type:%d, msg:%s", TAG, c, type, msg);
            return;
        }
        SocketWire.Packet packet = SocketWire.Packet.obtain(SocketWire.Packet.TYPE_MSG, type);
        packet.setParsedData(msg);
        c.socketWire.sendPacket(packet);
    }

    private void setupServer() {
        if (mNetworkChangeHelper.hasActiveNetwork()) {
            if (mState == STATE_STARTED || mState == STATE_STARTING) return;
            mSocketAccepter.setup(mPort, mAcceptListener);
            setState(STATE_STARTING);
        } else {
            if (L.loggable(C.CHANNEL, L.DEBUG)) {
                L.d(C.CHANNEL, "%s~ setupServer hold on by no network", TAG);
            }
            mSocketAccepter.teardown();
            setState(STATE_WAIT_NETWORK);
        }
    }

    private void serverSocketSetuped() {
        setState(STATE_STARTED);
    }

    private void newClientSocket(Socket socket) {
        if (L.loggable(C.CHANNEL, L.DEBUG)) {
            L.d(C.CHANNEL, "%s~ new client socket:%s", TAG, socket);
        }
        InetSocketAddress address = (InetSocketAddress) socket.getRemoteSocketAddress();
        String ip = address.getAddress().getHostAddress();
        ClientInfo clientInfo = findOldClient(ip);
        // Just disconnect old client.
        if (clientInfo != null) {
            clientInfo.socketWire.release();
        }
        SocketWire wire = new SocketWire(C.CHANNEL, socket, mWireListener, C.WIRE_CONFIG);
        NewerClientInfo info = new NewerClientInfo(wire);
        mNewerClientInfoByIp.put(info.ip, info);
        // We need make sure this session register itself in 3s, otherwise
        // disconnect.
        info.waitRegistTimeoutTask = bind(didWaitRegistTimeout, this, info).task();
        mCreateLoop.postTaskDelayed(info.waitRegistTimeoutTask, CLIENT_REGISTER_TIMEOUT);
    }

    @EarlCall
    private void didWaitRegistTimeout(NewerClientInfo info) {
        info.wire.release();
        mNewerClientInfoByIp.remove(info.ip);
    }

    private void newClientMsg(SocketWire wire, int type, Object msg) {
        if (msg == null) {
            L.w(C.CHANNEL, "%s~ newClientMsg[%s], null msg, type:%d", TAG, wire.getPeerIp(), type);
            return;
        }
        String ip = wire.getPeerIp();
        NewerClientInfo newerClientInfo = mNewerClientInfoByIp.get(ip);
        if (newerClientInfo != null) {
            switch (type) {
                case C.MSG_HELLO:
                    C.MsgHello hello = (C.MsgHello) msg;
                    ClientInfo realInfo = new ClientInfo();
                    realInfo.clientVersion = hello.clientVersion;
                    realInfo.ip = ip;
                    realInfo.pkg = hello.pkg;
                    realInfo.socketWire = newerClientInfo.wire;
                    /**
                     * realInfo will set when we got all client screen ids, and it will not changed by
                     * other way, so when this [screen id] client connected, pre-stored client info in
                     * mClientInfosByIp should update to real info.
                     */
                    mClientInfoByIp.put(ip, realInfo);
                    mNewerClientInfoByIp.remove(ip);
                    newerClientInfo.cancelPendingTask();
                    setNewClientState(realInfo, ClientInfo.STATE_REGISTED);
                    if (L.loggable(C.CHANNEL, L.DEBUG)) {
                        L.d(C.CHANNEL, "%s~ client[%s] register with pkg:%s, version:%d", TAG,
                                realInfo.ip, realInfo.pkg, realInfo.clientVersion);
                    }
                    // Send state
                    sendMsgToClient(realInfo, C.MSG_HELLO_ACK, new C.MsgHelloAck(mServerVersion));
                    break;
                default:
                    if (L.loggable(C.CHANNEL, L.DEBUG)) {
                        L.d(C.CHANNEL, "%s~ client[%s] not excepted msg before regist:%s", TAG, ip,
                                msg);
                    }
                    break;
            }
            return;
        }
        ClientInfo clientInfo = mClientInfoByIp.get(wire.getPeerIp());
        if (clientInfo == null) return;
        if (mState != STATE_STOP) {
            for (ServerHandler l : mServerLisOteners) {
                if (l.onNewClientMsg(clientInfo, type, msg)) {
                    mServerLisOteners.finishIterator();
                    break;
                }
            }
        }
    }

    private void clientConnected(SocketWire wire) {
        ClientInfo info = mClientInfoByIp.get(wire.getPeerIp());
        if (info == null) return;
        setNewClientState(info, ClientInfo.STATE_CONNECTED);
        if (L.loggable(C.CHANNEL, L.DEBUG)) {
            L.d(C.CHANNEL, "%s~ client[%s] connected!", TAG, info.ip);
        }
    }

    private void clientDisconnected(SocketWire wire, int reason, String detail) {
        String peerIp = wire.getPeerIp();
        ClientInfo info = mClientInfoByIp.remove(peerIp);
        if (info == null) return;
        setNewClientState(info, ClientInfo.STATE_DISCONNECTED);
        if (L.loggable(C.CHANNEL, L.DEBUG)) {
            L.d(C.CHANNEL, "%s~ client[%s] disconnected!", TAG, info.ip);
        }
    }

    private void setNewClientState(ClientInfo info, int state) {
        if (info.state == state) return;
        info.state = state;
        if (L.loggable(C.CHANNEL, L.DEBUG)) {
            L.d(C.CHANNEL, "%s~ new client[%s - %d] state: %s", TAG, info.pkg,
                    info.clientVersion, ClientInfo.getStateString(state));
        }
        if (mState != STATE_STOP) {
            for (ServerHandler l : mServerLisOteners) {
                l.onClientStateChanged(info);
            }
        }
    }

    private ClientInfo findOldClient(String ip) {
        for (ClientInfo c : mClientInfoList) {
            if (ip.equals(c.ip)) return c;
        }
        return null;
    }

    private void setState(@State int state) {
        if (mState == state) return;
        if (L.loggable(C.CHANNEL, L.DEBUG)) {
            L.d(C.CHANNEL, "%s~ state changed: %s -> %s", TAG, stateToString(mState),
                    stateToString(state));
        }
        mState = state;
        boolean ready = state == STATE_STARTED;
        if (ready == mServerReady) return;
        mServerReady = ready;
        for (ServerHandler l : mServerLisOteners) {
            l.onServerStateChanged(ready);
        }
    }

    public boolean isServerReady() {
        return mServerReady;
    }

    public void teardown() {
        mSocketAccepter.teardown();
        mNetworkChangeHelper.removeNetworkListener(mNetworkListener);
        mClientInfoList.clear();
        setState(STATE_STOP);
        for (NewerClientInfo info : mNewerClientInfoByIp.values()) {
            info.wire.release();
            info.cancelPendingTask();
        }
        mNewerClientInfoByIp.clear();
        while (!mClientInfoByIp.isEmpty()) {
            int size = mClientInfoByIp.size();
            Iterator<ClientInfo> iterator = mClientInfoByIp.values().iterator();
            ClientInfo info = iterator.next();
            info.socketWire.release();
            if (mClientInfoByIp.size() == size && size > 0) {
                iterator.remove();
            }
        }
        mClientInfoByIp.clear();
        mPort = 0;
    }

    public static class ClientInfo {
        public static final int STATE_DISCONNECTED = 1;
        public static final int STATE_CONNECTED = 2;
        public static final int STATE_REGISTED = 3;
        public static final int STATE_READY = 4;
        //
        @State
        public int state = STATE_DISCONNECTED;
        public String ip;
        public long clientVersion;
        public String pkg;
        SocketWire socketWire;

        @IntDef({STATE_DISCONNECTED, STATE_CONNECTED, STATE_REGISTED, STATE_READY})
        @Retention(RetentionPolicy.SOURCE)
        public @interface State {
        }

        public static String getStateString(int state) {
            switch (state) {
                case STATE_CONNECTED:
                    return "connected";
                case STATE_DISCONNECTED:
                    return "disconnected";
                case STATE_REGISTED:
                    return "registed";
                case STATE_READY:
                    return "ready";
                default:
                    return "unknow-" + state;
            }
        }

        @Override
        public String toString() {
            return "ClientInfo{" +
                    "state=" + state +
                    ", ip='" + ip + '\'' +
                    ", clientVersion=" + clientVersion +
                    ", pkg='" + pkg + '\'' +
                    ", socketWire=" + socketWire +
                    '}';
        }
    }

    private static class NewerClientInfo {
        public final SocketWire wire;
        public final String ip;
        Task waitRegistTimeoutTask;

        private NewerClientInfo(SocketWire wire) {
            this.wire = wire;
            ip = wire.getPeerIp();
        }

        public void cancelPendingTask() {
            if (waitRegistTimeoutTask != null) {
                waitRegistTimeoutTask.cancel();
                waitRegistTimeoutTask = null;
            }
        }
    }

    private static final Method1_0<ChannelServer, Void, NewerClientInfo> didWaitRegistTimeout = new Method1_0<ChannelServer, Void, NewerClientInfo>(ChannelServer.class, "didWaitRegistTimeout") {
        @Override
        public Void run(ChannelServer target, @NonNull Params1<NewerClientInfo> params) {
            target.didWaitRegistTimeout(params.p1);
            return null;
        }
    };
}
